import hashlib
import os
from dataclasses import dataclass

import xlsxwriter
from flask import request, jsonify, g, send_file
import jwt
import functools

from aab.exceptions import AabException
from .model import DailyState, ApprovalStates, CoPerson, db, Roles, User, Approval
from .settings import config
from datetime import datetime

DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_FORMAT = '%Y-%m-%d'


class AuthData:
    def __init__(self, user_number, group, role):
        self.user_number = user_number
        self.group = group
        self.role = role

    def to_dict(self):
        return {
            'user_number': self.user_number,
            'group': self.group,
            'role': self.role
        }

    @staticmethod
    def from_dict(d):
        return AuthData(**d)


class TokenException(Exception):
    pass


def generate_token(auth_data: AuthData, expire_time: datetime):
    token = jwt.encode({
        'auth': auth_data.to_dict(),
        'expire_time': expire_time.strftime(DATETIME_FORMAT)
    }, config.web.secret, algorithm='HS256')
    return token.decode('utf-8')


def check_token(token) -> AuthData:
    try:
        payload = jwt.decode(token, config.web.secret, algorithms=['HS256'])
    except jwt.PyJWTError as ex:
        raise TokenException(ex)

    expire_time = datetime.strptime(payload['expire_time'], DATETIME_FORMAT)
    if expire_time <= datetime.now():
        raise TokenException('token expired')

    auth_data = AuthData.from_dict(payload['auth'])
    return auth_data


def jsonify_error(code, data=None, **kwargs):
    data = data or dict()

    if isinstance(data, str):
        data = {
            'message': data
        }

    data.update(kwargs)

    if 'success' not in data:
        data['success'] = False

    response = jsonify(data)
    response.status_code = code
    return response


def login_required(roles=None, need_user=False):
    if isinstance(roles, str):
        roles = [roles]

    def decorator(view_func):
        @functools.wraps(view_func)
        def _wrapper(*args, **kwargs):
            token = request.headers.get('X-Token', None) or request.args.get('x-token', None)
            if token is None:
                return jsonify_error(401, message="No token provided")

            try:
                auth_data = check_token(token)
            except TokenException as ex:
                return jsonify_error(401, message="check token failed: {}".format(ex))

            if roles and not callable(roles) and auth_data.role not in roles:
                return jsonify_error(403, message='Forbidden')

            g.auth = auth_data

            if need_user:
                g.user = User.query.filter_by(number=auth_data.user_number).scalar()
                if g.user is None:
                    return jsonify_error(401, message='User not found')

            return view_func(*args, **kwargs)

        return _wrapper

    if callable(roles):
        return decorator(roles)

    return decorator


def hash_password(s):
    s = str(s).encode('utf-8')
    m = hashlib.sha256()
    m.update(s)
    return m.hexdigest()


def success(result=None):
    return jsonify({
        'success': True,
        'result': result
    })


def fill_worksheet(worksheet, entity, i, fields):
    d = entity.to_dict()
    for j, field in enumerate(fields):
        col = field[1]
        if callable(col):
            value = col(entity)
        else:
            value = str(d.get(col, '') or '')
        worksheet.write(i + 1, j + 1, value)


def format_health(d: DailyState):
    return '{}{}'.format(d.health,
                         '（{}）'.format(d.health_detail) if d.health_detail else '')


def format_persons(d: DailyState):
    return '；'.join(['{}，身体{}，体温{}'.format(person.relation, format_health(person), person.temperature)
                     for person in d.co_persons])


def approval_to_dict(approval):
    if approval is None:
        return {
            "state": ApprovalStates.WAIT_GROUP
        }
    else:
        approval_dict = approval.to_dict()
        approval_dict['group_manager'] = approval.group_manager.number if approval.group_manager else None
        approval_dict['admin'] = approval.admin.number if approval.admin else None
        return approval_dict


def update_persons(data, **kwargs):
    co_persons = CoPerson.query.filter_by(**kwargs).all()
    person_map = {p.id: p for p in co_persons}
    if 'persons' in data:
        for person in data['persons']:
            person_id = person.get('id', None)
            if person_id is not None and person_id in person_map:
                person_map[person_id].update_from_dict(person)
                del person_map[person_id]
            else:
                co_person = CoPerson(**kwargs).update_from_dict(person)
                db.session.add(co_person)
    for _, person in person_map.items():
        db.session.delete(person)


def do_approval(entity, is_reject, to_admin):
    user_number = g.auth.user_number
    user = User.query.filter_by(number=user_number).scalar()
    if user is None:
        return jsonify_error(403, "user not found")

    if entity.approval is None:
        entity.approval = Approval(fill_time=datetime.now())
        db.session.add(entity.approval)
    else:
        entity.approval.fill_time = datetime.now()

    role = g.auth.role
    if role == Roles.ADMIN:
        entity.approval.admin = user
        if is_reject:
            entity.approval.state = ApprovalStates.REJECT_ADMIN
        else:
            entity.approval.state = ApprovalStates.COMPLETED
    else:
        entity.approval.group_manager = user
        if is_reject:
            entity.approval.state = ApprovalStates.REJECT_GROUP
        elif to_admin:
            entity.approval.state = ApprovalStates.WAIT_ADMIN
        else:
            entity.approval.state = ApprovalStates.COMPLETED

    db.session.commit()
    return success()


@dataclass
class SheetCellFormat:
    handle: int
    row_span: int = 1
    col_span: int = 1


class SheetColumn:
    def __init__(self):
        self.data_list = []
        self.width = 10

    def write(self, i, data, cell_format: SheetCellFormat = None):
        if len(self.data_list) <= i:
            for _ in range(i - len(self.data_list) + 1):
                self.data_list.append(None)
        self.data_list[i] = SheetCell(data, cell_format)
        if data and (cell_format is None or not cell_format.row_span or cell_format.col_span < 2):
            self.width = max(self.width, len(str(data).encode('gbk', errors='replace')))


class SheetCell:
    def __init__(self, value, cell_format=None):
        self.value = value
        self.format = cell_format


class SheetData:
    def __init__(self, path):
        self.columns = []
        if not path.startswith('/'):
            path = os.path.join('/tmp', path)
        self.path = path
        self.format_option_list = []

        self.default_header_format_options = {
            'bold': 1,
            'align': 'center',
            'valign': 'vcenter'
        }

        self.default_header_format = self.add_format(self.default_header_format_options)

    def write(self, i, j, data, cell_format=None):
        if len(self.columns) <= j:
            for _ in range(j - len(self.columns) + 1):
                self.columns.append(SheetColumn())

        col = self.columns[j]
        col.write(i, data, cell_format)

    def add_format(self, options=None, row_span=1, col_span=1):
        handle = len(self.format_option_list)
        self.format_option_list.append(options or dict())
        return SheetCellFormat(handle=handle, row_span=row_span, col_span=col_span)

    def multi_span_header_format(self, row_span=1, col_span=1):
        return self.add_format(self.default_header_format_options, row_span=row_span, col_span=col_span)

    def write_header(self, i, j, data, row_span=1, col_span=1):
        if row_span == 1 and col_span == 1:
            self.write(i, j, data, self.default_header_format)
        else:
            self.write(i, j, data, self.multi_span_header_format(row_span=row_span, col_span=col_span))

    def write_to_xlsx(self):
        workbook = xlsxwriter.Workbook(self.path)

        format_list = [workbook.add_format(options) for options in self.format_option_list]

        try:
            worksheet = workbook.add_worksheet()
            for j, col in enumerate(self.columns):
                worksheet.set_column(j, j, col.width)
                for i, cell in enumerate(col.data_list):
                    if cell is not None:
                        if cell.format is None:
                            worksheet.write(i, j, cell.value)
                        else:
                            format_option = format_list[cell.format.handle]

                            if cell.format.row_span <= 1 and cell.format.col_span <= 1:
                                worksheet.write(i, j, cell.value, format_option)
                            else:
                                worksheet.merge_range(i, j,
                                                      i + cell.format.row_span - 1,
                                                      j + cell.format.col_span - 1,
                                                      cell.value, format_option)
        finally:
            workbook.close()

    def send_file(self):
        self.write_to_xlsx()
        return send_file(self.path, as_attachment=True, cache_timeout=0)


def excel_col_char(col):
    if col == 0:
        return 'A'

    c = ''
    remain = col
    while remain > 0:
        c = chr(ord('A') + (remain % 26)) + c
        remain //= 26

    return chr(ord(c[0]) - 1) + c[1:]
